package cz.muni.fi.xvano1.trinity.core;

import cz.muni.fi.xvano1.trinity.supplementary.CalendarHandler;
import java.io.*;
import java.net.SocketException;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.data.CalendarOutputter;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.model.*;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.property.*;
import net.fortuna.ical4j.util.UidGenerator;

/**
 * This class acts as a base for operations with iCalendar files. It is
 * instanced with a user name. Consequently all user data is stored under
 * 'user'.ics. All basic operations are provided. This core class is based on
 * Ben Fortuna's iCal4J (ical4j.sourceforge.net). Note that this as well as
 * iCal4J works with iCalendar format from RFC 2445, not the newer one (RFC
 * 5545). To have a RFC 5545 compliant iCalendar implementation, one would have
 * to comply with Appendix A of RFC 5545.
 *
 * User file will be stored in $HOME/trinity/data/userFiles/. Class furthermore
 * expects FlexNet Publisher license configurations file 'license.dat' in
 * $HOME/trinity/data/flexLimits/. Last of all the class has fields expressing
 * internal interpretations such as NONE/DAILY/MONTHLY/WEEKLY which are
 * basically constants for methods and therefore named refference can be used
 * instead of numbers. Other fields are regular expressions for input
 * validations and paths to datas in the file system for thesis work that uses
 * this class (https://is.muni.cz/th/325170/fi_b/) and can be removed in case of
 * use for other projects.
 *
 * @author Jan Vano
 */
public class Core {

    /*
     * Following are variable declarations. Variable 'user' will be used for
     * instancing and operations with concrete user file. Variable 'lastMessage'
     * will be used for displaying messages to user (e.g. when input is
     * incorrect and/or what is expected.)
     */
    private static final String prodId = "-//FI MUNI//iCal4j 1.0//CZ";
    private final String user;
    private final String userBase;
    /**
     * Expected location of user *.ics files.
     */
    public static final String userDirectory = System.getenv("HOME") + "/trinity/data/userFiles/";
    /**
     * Expected location of limits.dat.
     */
    public static final String toolboxDirectory = System.getenv("HOME") + "/trinity/data/flexLimits/";
    /**
     * Expected location of configuration.
     */
    public static final String configDirectory = System.getenv("HOME") + "/trinity/data/conf/";
    /**
     * Expected location of output ('array.dat')of complementary thesis work.
     */
    public static final String calendar = System.getenv("HOME") + "/trinity/lmcal/data/response/array.dat";
    /**
     * Expected location of output ('response.rs')of complementary thesis work.
     */
    public static final String negativeResponse = System.getenv("HOME") + "/trinity/lmcal/data/response/response.rs";
    /**
     * Expected location of complementary thesis work location.
     */
    public static final String lmcal = System.getenv("HOME") + "/trinity/lmcal/";
    /**
     * Expected location of complementary thesis work executable files location.
     */
    public static final String scripts = lmcal + "executables/";
    public static final int NONE = 0;
    public static final int DAILY = 1;
    public static final int WEEKLY = 2;
    public static final int MONTHLY = 3;
    private static final String regex = "[a-zA-Z0-9]+(_tmp)?";
    /**
     * Regular expression for summary parameter.
     */
    public static final String summaryRegex = "[a-zA-Z0-9]+[a-zA-Z0-9_ \\(\\),]*[a-zA-Z0-9\\(\\),]+";
    /**
     * Regular expression for description parameter.
     */
    public static final String descriptionRegex = "[a-zA-Z0-9_\\- [\\\\,]*]+";
    /**
     * Regular expression for date part of date-time parameter.
     */
    public static final String dateRegex = "(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\\d\\d";
    /**
     * Regular expression for time part of date-time parameter.
     */
    public static final String timeRegex = "([0-1][0-9]|2[0-3]):([0-5][0-9])";
    /**
     * Constant expressing GMT zone of server.
     */
    public final String gmt;
    /*
     * Last message available to user. Mainly used for displaying error messages
     * to user through interface and to administrator via std error output.
     */
    private String lastMessage = "";

    /**
     * Constructor for a specific user
     *
     * @param user User name. Name has to be alphanumeric.
     * @throws IllegalArgumentException Exception is throwed if username is not
     * alphanumeric.
     */
    public Core(String user) {
        if ((user == null) || ("".equals(user))) {
            throw new IllegalArgumentException("User name unspecified!");
        }
        if (!(user.matches(regex))) {
            throw new IllegalArgumentException("User name not alphanumeric!");
        }
        gmt = "Europe/Prague";
        this.user = user;
        if (user.contains("_tmp")) {
            userBase = user.split("_tmp")[0];
        } else {
            userBase = user;
        }
    }

    /**
     * Method for getting last available message.
     *
     * @return The last message available to user (information,errors, etc).
     */
    public final String getLastMessage() {
        return lastMessage;
    }

    /**
     * Method for checks whether the user file is parseable. If the file is not
     * parseable, it's name is changed (prefix old_ is added).
     *
     * @return Integer specifying result state: 0 - passed correctly; 1 - the
     * file is not present; 2 - unparseable file is present; 3 - unparseable
     * file could not be renamed
     */
    public int checkFileIntegrity() {
        File file = new File(userDirectory + user + ".ics");
        File renamedFile = new File(userDirectory + "old_" + user + ".ics");

        if (file.exists()) {
            Calendar calendar = load(userDirectory + user + ".ics");
            if (calendar == null) {
                boolean result = file.renameTo(renamedFile);
                if (result) {
                    lastMessage = "File " + user + ".ics was not parseable. "
                            + "File could not be renamed.";
                    System.err.println(lastMessage);
                    lastMessage = lastMessage + " Please contact administrator.";
                    return 3;
                } else {
                    lastMessage = "File " + user + ".ics was not parseable. "
                            + "It was renamed so that a new file can be created.";
                    System.err.println(lastMessage);
                    return 2;
                }
            }
            lastMessage = "File is in acceptable state.";
            return 0;
        }
        lastMessage = "File was not found.";
        System.err.println(lastMessage);
        return 1;
    }

    /*
     * Method tries to open input stream to specified location. If it succeeds,
     * it returns object for that input stream. Otherwise null is returned.
     */
    private FileInputStream getInputStream(String target) {
        if (target == null) {
            System.err.println("Null pointer in getInputStream call!");
            return null;
        }
        FileInputStream fin = null;
        try {
            fin = new FileInputStream(target);
        } catch (FileNotFoundException ex) {
            System.out.println("User " + user + " file not present.");
            return null;
        } catch (Exception ex) {
            Logger.getLogger(Core.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
        return fin;
    }

    /*
     * This method tries to open output stream to specified location. If it
     * succeeds, it returns object for that output stream. Otherwise null is
     * returned.
     */
    private FileOutputStream getOutputStream(String target) {
        if (target == null) {
            System.err.println("Null pointer in getOutputStream call!");
            return null;
        }
        FileOutputStream fout = null;
        try {
            fout = new FileOutputStream(target);
        } catch (FileNotFoundException ex) {
            System.out.println("User " + user + " file not present.");
            return null;
        } catch (Exception ex) {
            Logger.getLogger(Core.class.getName()).log(Level.WARNING, null, ex);
            return null;
        }
        return fout;
    }

    /**
     * Parses .ics file specified by user. User value is initialized in
     * constructor.
     *
     * @return net.fortuna.ical4j.model.Calendar object representing data; null
     * file could not be read - in that case user needs to call getLastMessage()
     */
    public Calendar load() {
        return load(userDirectory + user + ".ics");
    }

    /*
     * Parses .ics file at specified location and loads the calendar structure.
     * If it is not able to load the calendar, it returns null.
     */
    private Calendar load(String target) {
        if (target == null) {
            System.err.println("Null pointer in load call!");
            lastMessage = "Null pointer in load call!";
            return null;
        }
        FileInputStream fin = getInputStream(target);
        if (fin == null) {
            lastMessage = "File is missing.";
            return null;
        }
        CalendarBuilder builder = new CalendarBuilder();
        Calendar calendar = null;
        try {
            calendar = builder.build(fin);
        } catch (IOException ex) {
            Logger.getLogger(Core.class.getName()).log(Level.SEVERE, "I/O exception occured!", ex);
            lastMessage = "There was I/O error.";
            try {
                fin.close();
            } catch (IOException ex1) {
                Logger.getLogger(Core.class.getName()).log(Level.SEVERE, null, ex1);
            }
            return null;
        } catch (ParserException ex) {
            Logger.getLogger(Core.class.getName()).log(Level.SEVERE, "Exception while parsing!", ex);
            lastMessage = "There was error while loading your file. It may be corrupted.";
            try {
                fin.close();
            } catch (IOException ex1) {
                Logger.getLogger(Core.class.getName()).log(Level.SEVERE, null, ex1);
            }
            return null;
        } finally {
            try {
                fin.close();
            } catch (IOException ex) {
                Logger.getLogger(Core.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return calendar;
    }

    /*
     * Writes calendar object file to disk at specified location. If it fails
     * due to whatever reason, it returns integer specifying the error. Return
     * codes: 0 - ok 1 - null pointer call 2 - file was missing 3 - I/O error 4
     * - target file was not parseable
     */
    private int write(Calendar cal, String target) {
        if ((target == null) || (cal == null)) {
            System.err.println("Null pointer in write call!");
            lastMessage = "Null pointer in write call!";
            return 1;
        }
        FileOutputStream fout = getOutputStream(target);
        if (fout == null) {
            lastMessage = "File is missing.";
            return 2;
        }
        CalendarOutputter outputter = new CalendarOutputter();
        try {
            outputter.output(cal, fout);
        } catch (IOException ex) {
            Logger.getLogger(Core.class.getName()).log(Level.SEVERE, "I/O exception occured!", ex);
            lastMessage = "There was I/O error.";
            try {
                fout.close();
            } catch (IOException ex1) {
                Logger.getLogger(Core.class.getName()).log(Level.SEVERE, null, ex1);
            }
            return 3;
        } catch (ValidationException ex) {
            Logger.getLogger(Core.class.getName()).log(Level.SEVERE, "Provided calendar is not valid!", ex);
            lastMessage = "Error while storing your data. Make sure that all records are valid (and have been suplied.)";
            try {
                fout.close();
            } catch (IOException ex1) {
                Logger.getLogger(Core.class.getName()).log(Level.SEVERE, null, ex1);
            }
            return 4;
        }
        try {
            fout.close();
        } catch (IOException ex) {
            Logger.getLogger(Core.class.getName()).log(Level.SEVERE, null, ex);
        }
        return 0;
    }

    /**
     * Loads calendar file with specified UID and file location.
     *
     * @param uid UID of event that need to be loaded from calendar.
     * @return VEvent object if successful. Otherwise null.
     */
    public VEvent loadEvent(String uid) {
        if (uid == null) {
            System.err.println("Null pointer in loadEvent call!");
            lastMessage = "Null pointer in loadEvent call!";
            return null;
        }
        Calendar calendar = load(userDirectory + user + ".ics");
        if (calendar == null) {
            return null;
        }
        ComponentList source = calendar.getComponents();
        VEvent foundEvent = null;
        for (int i = 0; i < source.size(); i++) {
            VEvent actualEvent = (VEvent) source.get(i);
            if ((actualEvent).getUid().getValue().equals(uid)) {
                foundEvent = actualEvent;
                break;
            }
        }
        if (foundEvent == null) {
            lastMessage = "Event with specified UID not found.";
        }
        return foundEvent;
    }

    /**
     * Takes the source event and change it in calendar. Then it writes calendar
     * back to disk.
     *
     * @param event Event which exists in user calendar and is meant to be
     * changed.
     * @return 0 - passed correctly; 1 - for null pointer; 2 - for invalid or
     * not existing calendar; 3 - if no event has UID of the supplied event
     */
    @SuppressWarnings("unchecked")
    public int changeEvent(VEvent event) {
        if (event == null) {
            System.err.println("Null pointer in changeEvent call!");
            lastMessage = "Null pointer in changeEvent call!";
            return 1;
        }
        String uid = event.getUid().getValue();
        Calendar calendar = load(userDirectory + user + ".ics");
        if (calendar == null) {
            return 2;
        }
        ComponentList source = calendar.getComponents();
        int foundIndex = -1;
        for (int i = 0; i < source.size(); i++) {
            VEvent actualEvent = (VEvent) source.get(i);
            if ((actualEvent).getUid().getValue().equals(uid)) {
                foundIndex = i;
                break;
            }
        }
        if (foundIndex == -1) {
            lastMessage = "Event with specified UID not found.";
            return 3;
        } else {
            calendar.getComponents().set(foundIndex, event);
        }
        write(calendar, userDirectory + user + ".ics");
        return 0;
    }

    /**
     * Takes the uid of target event and tries to remove that event in calendar.
     * If it succeeds to find the event, calendar is altered - event is removed.
     * Calendar is then written back to disk.
     *
     * @param uid UID of event that needs to be deleted.
     * @return 0 - passed correctly; 1 - null pointer call; 2 - invalid or not
     * existing calendar was present; 3 - no event has supplied UID
     */
    public int removeEvent(String uid) {
        if (uid == null) {
            System.err.println("Null pointer in removeEvent call!");
            lastMessage = "Null pointer in removeEvent call!";
            return 1;
        }
        Calendar calendar = load(userDirectory + user + ".ics");
        if (calendar == null) {
            return 2;
        }
        ComponentList source = calendar.getComponents();
        int foundIndex = -1;
        for (int i = 0; i < source.size(); i++) {
            VEvent actualEvent = (VEvent) source.get(i);
            if ((actualEvent).getUid().getValue().equals(uid)) {
                foundIndex = i;
                break;
            }
        }
        if (foundIndex == -1) {
            lastMessage = "Event with specified UID not found.";
            return 3;
        } else {
            calendar.getComponents().remove(calendar.getComponents().get(foundIndex));
        }
        write(calendar, userDirectory + user + ".ics");
        lastMessage = "Event with UID: +" + uid + " was found and removed.";
        return 0;
    }

    /**
     * Iterates over Calendar objects and provides the output via String.
     *
     * @param target Target iCalendar file which will be parsed to String.
     * @return Simplified version of calendar used solely for a quick view on
     * it's content.
     */
    public String printCalendar(String target) {
        Calendar calendar = load(target);
        if (calendar == null) {
            return "Null pointer call!!";
        }
        String result = "";
        for (Iterator i = calendar.getComponents().iterator(); i.hasNext();) {
            Component component = (Component) i.next();
            result = result + "Component [" + component.getName() + "]: ";
            for (Iterator j = component.getProperties().iterator(); j.hasNext();) {
                Property property = (Property) j.next();
                result = result + "Property [" + property.getName() + ", " + property.getValue() + "], ";
            }
            result = result + "\n";
        }
        return result;
    }

    /**
     * Creates new VEvent with specified parameters.
     *
     * @param name Description of event.
     * @param start Start of the event in 'DD.MM.YYYY HH:MM' format.
     * @param end End of the event in 'DD.MM.YYYY HH:MM' format.
     * @param recur Number specifying recurrence. 0 means none, 1 means daily, 2
     * means weekly and 3 represents monthly recurrence.
     * @param wholeDay Boolean that represents whether the event will last whole
     * day. If so, day from startDay is used and format DD.MM.YYYY is
     * sufficient.
     * @param description Longer description of event.
     * @param location Location of event.
     * @return VEvent object if the method passed correctly. Null otherwise.
     */
    public VEvent createEvent(String name, String start, String end, int recur, boolean wholeDay, String description, String location) {

        int inputStatus = SecondaryParser.checkNameDescLoc(name, description, location);
        if (inputStatus != 0) {
            if ((inputStatus == 1) || (inputStatus == 4)) {
//              Supplied with wrong event name.
                lastMessage = "Wrong event description. Description must start with alphanum char and must not end with space. Further allowed chars are: '\\(),_'.";
            }
            if ((inputStatus == 2) || (inputStatus == 5)) {
//              Supplied with wrong event description (desc is supplied via toolbox options).
                lastMessage = "Supplied with wrong event description. You have not chosen any toolboxes.";
            }
            if ((inputStatus == 3) || (inputStatus == 6)) {
                lastMessage = "Supplied with wrong event location.";
            }
            return null;
        }
        if (wholeDay) {
            inputStatus = SecondaryParser.checkSingleDateTime(start);
            if (inputStatus != 0) {
                lastMessage = "Wrong time-date format of start.";
                return null;
            }
        } else {
            inputStatus = SecondaryParser.checkDateTime(start, end);
            if (inputStatus != 0) {
                lastMessage = "Wrong time-date format or data. Note that event"
                        + " may start/end yesterday at earliest and year ahead at latest.";
                return null;
            }
        }
        VEvent event = null;
        TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry();
        TimeZone timezone = registry.getTimeZone(gmt);
        java.util.Calendar timeFrom = CalendarHandler.getCalendarForDateTime(start);
        timeFrom.setTimeZone(timezone);
        java.util.Calendar timeTo = CalendarHandler.getCalendarForDateTime(start);;
        timeTo.setTimeZone(timezone);
        if (!(wholeDay)) {
            // initialise as an event ocuring some time during the day
            timeFrom.set(java.util.Calendar.SECOND, 0);
            timeFrom.set(java.util.Calendar.MILLISECOND, 0);
            timeTo = CalendarHandler.getCalendarForDateTime(end);
            timeTo.set(java.util.Calendar.SECOND, 0);
            timeTo.set(java.util.Calendar.MILLISECOND, 0);
        } else {
            // initialise as an all-day event..
            timeFrom.set(java.util.Calendar.HOUR_OF_DAY, 0);
            timeFrom.set(java.util.Calendar.MINUTE, 0);
            timeFrom.set(java.util.Calendar.SECOND, 0);
            timeFrom.set(java.util.Calendar.MILLISECOND, 0);
            int startYear = timeFrom.get(java.util.Calendar.YEAR);
            int startMonth = timeFrom.get(java.util.Calendar.MONTH);
            int startDay = timeFrom.get(java.util.Calendar.DATE);
            timeTo.set(startYear, startMonth, startDay + 1, 0, 0, 0);
            timeTo.set(java.util.Calendar.MILLISECOND, 0);
        }
        DateTime startDateTime = new DateTime(timeFrom.getTime());
        DateTime endDateTime = new DateTime(timeTo.getTime());
        startDateTime.setTimeZone(timezone);
        endDateTime.setTimeZone(timezone);
        event = new VEvent(startDateTime, endDateTime, name);
        // set recurrence
        String recurrence = "";
        java.util.Calendar untilCalendar = java.util.Calendar.getInstance();
        untilCalendar.add(java.util.Calendar.YEAR, 1);
        Date untilDate = new Date(untilCalendar.getTime());
        switch (recur) {
            case NONE:
                break;
            case DAILY:
                recurrence = Recur.DAILY;
                break;
            case WEEKLY:
                recurrence = Recur.WEEKLY;
                break;
            case MONTHLY:
                recurrence = Recur.MONTHLY;
                break;
            default:
                recur = NONE;
                break;
        }
        if (recur != NONE) {
            Recur recurP = new Recur(recurrence, untilDate);
            RRule rrule = new RRule(recurP);
            event.getProperties().add(rrule);
        }
        // Generate a UID for the event..
        UidGenerator ug = null;
        try {
            ug = new UidGenerator(userBase);
        } catch (SocketException ex) {
            Logger.getLogger(Core.class.getName()).log(Level.SEVERE, null, ex);
            System.err.println("Socket error while creating UID!");
            lastMessage = "There was an internal error while creating event. "
                    + "Please contact administrator.";
            return null;
        }
        event.getProperties().add(ug.generateUid());
        event.getProperties().add(new Location(location));
        event.getProperties().add(new Description(description));
        return event;
    }

    /*
     * Creates blank net.fortuna.ical4j.model.Calendar object ready to be filled
     * with components.
     */
    private Calendar createCalendar() {
        Calendar cal = new Calendar();
        cal.getProperties().add(new ProdId(prodId));
        cal.getProperties().add(Version.VERSION_2_0);
        cal.getProperties().add(CalScale.GREGORIAN);
        return cal;
    }

    /*
     * This method create iCal file with first event. Parameters are used for
     * specific values of the first event.
     */
    private int createFile(String name, String start, String end, int recur, boolean wholeDay, String description, String location) {
        VEvent firstEvent = createEvent(name, start, end, recur, wholeDay, description, location);
        if (firstEvent == null) {
            return 1;
        }
        Calendar cal = createCalendar();
        cal.getComponents().add(firstEvent);
        if (write(cal, userDirectory + user + ".ics") != 0) {
            return 2;
        }
        return 0;
    }

    /**
     * This method appends new event to the iCal file. If there is no file, it
     * is created with supplied parameters.
     *
     * @param name Description of event.
     * @param start Start of the event in 'DD.MM.YYYY HH:MM' format.
     * @param end End of the event in 'DD.MM.YYYY HH:MM' format.
     * @param recur Number specifying recurrence. 0 means none, 1 means daily, 2
     * means weekly and 3 represents monthly recurrence.
     * @param wholeDay Boolean that represents whether the event will last whole
     * day. If so, day from startDay is used and format DD.MM.YYYY is
     * sufficient.
     * @param description Longer description of event.
     * @param location Location of event.
     * @return Integer number representing results. 0 - event successfuly added;
     * 1 - event could not be created due to wrong parameters; 2 - error occured
     * when writing new calendar to disk 3 - target calendar could not be parsed
     * prior to modification
     */
    public int addEvent(String name, String start, String end, int recur, boolean wholeDay, String description, String location) {
        File file = new File(userDirectory + user + ".ics");
        if (!file.exists()) {
            return createFile(name, start, end, recur, wholeDay, description, location);
        }
        VEvent event = createEvent(name, start, end, recur, wholeDay, description, location);
        if (event == null) {
            return 1;
        }
        Calendar calendar = load(userDirectory + user + ".ics");
        if (calendar == null) {
            return 3;
        }
        calendar.getComponents().add(event);
        if (write(calendar, userDirectory + user + ".ics") != 0) {
            return 2;
        }
        return 0;
    }
}
